Function.RegisterNamespace("xUnit.js.Model");

xUnit.js.Model.Fact=function(method,name){
    // Private members
    var _events;
    var _mocks;
    var _skip;
    var _reason;

    // Public members
    this.Asynchronous=false;
    this.Data=null;
    this.DataIndex=null;
    this.Events;
    this.Method;
    this.Name;
    this.Parent;
    this.State;
    this.States;

    // ctor
    function Fact(method,name,parent){
        if(!Object.IsType(Function,method))throw new Error("xUnit.js.Model.Fact.ctor: 'method' must be a valid Function pointer.");
        _mocks=[];
        this.Events=_events=new System.Event.EventManager("BeforeRun","AfterRun");
        this.Method=method;
        this.Name=name||method&&Function.GetName(method)||"[Anonymous]";
        this.Parent=parent||null;
        this.State={
            Index:0,
            Message:'',
            Result:xUnit.js.Model.Result.Unknown
        };
        this.States=[this.State];
    }
    if(this.constructor==xUnit.js.Model.Fact)Fact.apply(this,arguments);
    
    this.AddMock=function(mock){
        if(!Object.IsType(Function,mock)||!mock.IsMock)throw new Error("xUnit.js.Model.Fact.AddMock: 'mock' must be a valid mockery.");
        _mocks.push(mock);
    };

    this.GetMocks=function(){
        var mocks=_mocks.slice();
        if(this.Parent)return this.Parent.GetMocks().concat(mocks);
        return mocks;
    };

    this.GetPath=function(){
        var path=[];
        var step=this;
        while(step){
            path.unshift(step.Name);
            step=step.Parent;
        }
        return path.join('.');
    };
    
    // IRunnable members
    this.Run=function(){
        var dataIndex=this.DataIndex||0;
        this.State.Result=xUnit.js.Model.Result.Unknown;
        var context=new xUnit.js.Model.EventContext(this,this.State.Result);
        _events.Fire("BeforeRun",context);
        try{
            var rawMethod=this.Method.GetDecoration&&this.Method.GetDecoration().GetMethod()||this.Method;
            if(_skip||context.Cancel){
                this.State.Message=_reason||"[No reason given]";
                this.State.Result=context.Result=xUnit.js.Model.Result.Skipped;
                _events.Fire("AfterRun",context);
            }else{
                if(Function.IsEmpty(rawMethod)){
                    throw new xUnit.js.Model.AssertError("No method body found. Assuming intentional failure (TDD).");
                }
                var asserts=Assert.Calls;
                var parameters=[];
                if(this.Asynchronous){
                    var callbackParam=Function.GetParameters(rawMethod)[0];
                    if(!callbackParam||!String.Contains(Function.GetBody(rawMethod),callbackParam))throw new xUnit.js.Model.AssertError("Asynchronous callback appears to be missing. Facts marked asynchronous must complete the test by invoking the callback method supplied.");
                    parameters.push(Function.GetDelegate(processRun,this,this,context,null,true,dataIndex));
                }
                if(Object.IsType(Array,this.Data)){
                    if(this.DataIndex==undefined||isNaN(this.DataIndex))this.DataIndex=0;
                    parameters.push(this.Data[this.DataIndex]);
                }
                processRun(this,context,parameters,false,dataIndex,this.Method);
                if(this.Asynchronous){
                    if(Assert.Calls!=asserts){
                        throw new xUnit.js.Model.Warning(String.Format("Single-Assert violation. Found {0} Asserts in main thread. Asserts in asynchronous Facts must be made in the delegate passed to the callback method provided to ensure synchronization, e.g.:\n  function {1}(callback){\n    callback(function(){\n      Assert.True(true);\n    });\n  }",Assert.Calls-asserts,this.Name));
                    }
                    this.States[dataIndex].Result=xUnit.js.Model.Result.Waiting;
                }
            }
        }catch(e){
            if(this.State.Result==xUnit.js.Model.Result.Unknown){
                registerError(this,dataIndex,e);
                _events.Fire("AfterRun",context);
            }
        }
        return this.State;
    };
    
    // ISkippable Members
    this.Skip=function(reason){
        _skip=true;
        _reason=reason;
    };

    // Private Methods
    function processRun(fact,context,parameters,inCallback,dataIndex,delegate){
        try{
            if(inCallback){
                if(fact.States[dataIndex].Result!=xUnit.js.Model.Result.Waiting)throw new xUnit.js.Model.AssertError("Fact is marked asynchronous, but callback was invoked synchronously.");
                if(!Object.IsType(Function,delegate))throw new xUnit.js.Model.AssertError("callback(delegate): 'delegate' was not a valid Function.");
            }
            var asserts=Assert.Calls;
            xUnit.js.Mocks.Mock.apply(xUnit.js.Mocks,fact.GetMocks())(function(){
                delegate.apply(Object.Global(),parameters||[]);
            });
            if(fact.Asynchronous&&!inCallback){
                return runNext(fact);
            }
            if((Assert.Calls-asserts)!=1){
                throw new xUnit.js.Model.Warning(String.Format("Single-Assert violation. Found {0} Asserts.",Assert.Calls-asserts));
            }
            fact.States[dataIndex].Result=xUnit.js.Model.Result.Success;
        }catch(e){
            registerError(fact,dataIndex,e);
        }
        context.Result=fact.States[dataIndex].Result;
        _events.Fire("AfterRun",context);
        runNext(fact);
    }

    function registerError(fact,dataIndex,error){
        fact.States[dataIndex].Message=error;
        if(error instanceof xUnit.js.Model.AssertError){
            fact.States[dataIndex].Result=xUnit.js.Model.Result.Failure;
        }else if(error instanceof xUnit.js.Model.Warning){
            fact.States[dataIndex].Result=xUnit.js.Model.Result.Warning;
        }else{
            fact.States[dataIndex].Result=xUnit.js.Model.Result.Error;
        }
    }

    function runNext(fact){
        if(Object.IsType(Array,fact.Data)&&fact.DataIndex<fact.Data.length-1){
            fact.DataIndex++;
            fact.State={Index:fact.DataIndex,Message:'',Result:xUnit.js.Model.Result.Unknown};
            fact.States.push(fact.State);
            fact.Run();
        }
    }
        
};

xUnit.js.Model.Fact.Implement(xUnit.js.Model.IRunnable,"xUnit.js.Model.Fact");
xUnit.js.Model.Fact.Implement(xUnit.js.Model.ISkippable,"xUnit.js.Model.Fact");
xUnit.js.Model.Fact.Implement(xUnit.js.Model.IMockable,"xUnit.js.Model.Fact");